package com.googlecode.skald.client.affordances;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import com.google.gwt.core.client.JavaScriptObject;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.JsArrayMixed;
import com.google.gwt.core.client.JsArrayString;
import com.google.gwt.core.client.JsonUtils;

/**
 * A JSON overlay object that represents a compact {@link Affordance}.
 * A compact Affordance may include more than a single affordance in 
 * compact notation.
 * <p>
 * Each affordance should be specified by the server as a JSON object.  
 * The following documents all of the supported fields for all possible 
 * Affordance JSON objects.  See each of the associated Affordance subclasses 
 * for more details about the subset of allowed fields and associated value 
 * constraints for that affordance subtype.
 * <p>
 * Field values may include embedded "variables" of the form $variableName.
 * The variables supported depend on the Affordance subtype, but supported
 * variable will be replaced with values before use.
 * <p>
 * Fields are not trimmed, and comparison made are case-sensitive.   
 * </p>
 *  
 * <h4>Supported Fields</h4>
 * <dl> 
 * <dt><code>type</code> (string; optional)
 * <dd>Currently ignored, but reserved for future use.
 * 
 * <dt><code>order</code> (number (int); optional)
 * <dd>The sort order and category of the affordance.
 * 
 * <dt><code>affordance</code> (array; required)
 * <dd>The essence of a player action, as displayed to the player.
 * At its simplest, this is simply an array of strings, where each element
 * corresponds to a display element of the affordance.  Examples include:
 * ["Look"], ["Get", "ball"], ["Push", "witch", "into", "bubbling cauldron"].
 * <p>
 * Normally the verb comes first.
 * <p>
 * For each element, it is possible to provide an array of strings instead.
 * This makes for more compact representations and may ease generation of 
 * affordances on the server side.  Examples: [["Look at", "Get"], "stick"],
 * ["Get", ["ball", "stick", "tea cup"], [["Get", "Examine"], ["ball", "stick"]].
 * When more than one element is an array, all possible combinations of all
 * arrays are produced.  That is, the last example given above would expand
 * to ["Get", "ball"], ["Get", "stick"], ["Examine", "ball"], ["Examine", 
 * "stick"]. 
 * <p>
 * Note that a compact affordance uses the same cmd string for all expanded
 * results.  However, the cmd string may use embedded variables.
 * 
 * <dt><code>cmd</code> (string; optional)
 * <dd>The command to send back to the server when this affordance is selected.
 * May allow use of embedded variables.
 * <p>
 * Any cmd may use variables of the form: $0, $1, $2, ... $9.
 * These act as indexes into the corresponding affordance array after 
 * expansion of any compact elements.  Other affordance types may offer
 * the option of additional variables.
 * <p>
 * If not given, sends a space-separated joined string of the affordance
 * elements.
 * 
 * <dt><code>weak</code> (boolean; optional) 
 * <dd>Whether this is a weak affordance.
 * </dl>
 * 
 * @author Zach Tomaszewski
 */
public class JsAffordance extends JavaScriptObject {

  //private static Logger logger = Logger.getLogger(Affordances.class.getName());
  
  //GWT tutorial says: Overlay types always have protected default constructors.
  protected JsAffordance() {} 
  
  //JSNI accessors  
  public final native JsArrayMixed affordance() /*-{ 
    return this.affordance;
  }-*/;
  
  public final native String cmd()  /*-{ 
    return this.cmd; 
  }-*/;

  public final native int order()  /*-{ 
    return (this.order) ? this.order : 0; 
  }-*/;
  
  public final native boolean weak()  /*-{ 
    return (this.weak) ? this.weak : false;
  }-*/;
    
  /** 
   * Converts the JSON string to convenient JsArray.  Does not confirm that
   * the array elements are valid.  See {@link #toAffordances(String)}
   * for that. 
   * 
   * @throws IllegalArgumentException  If string is not valid JSON.
   * @throws ClassCastException  If parses into valid JSON, but not an array.
   */
  public final static JsArray<JsAffordance> parse(String affords) {
    return JsonUtils.safeEval(affords);
  };  

  /**
   * Returns the given raw JavaScript String as a list of Affordances.
   *
   * @see #parse(String)
   * @see #toAffordances(JsArray)
   */
  public static List<Affordance> toAffordances(String jsAffords) {
    return toAffordances(parse(jsAffords));
  }
  
  /**
   * Given a JavaScript affordances array, parses it and returns
   * the resulting list of specific affordances.  Expands all compact 
   * affordances by producing an affordance instance for each possible 
   * combination of terms.
   * 
   * @throws InvalidFormatException if could not parse JavaScript list
   */
  public static List<Affordance> toAffordances(JsArray<JsAffordance> affords) {
    try {
      List<Affordance> list = new ArrayList<Affordance>();
      for (int i = 0; i < affords.length(); i++) {
        JsAffordance aff = affords.get(i);
        list.addAll(unpackAffordance(aff));
      }
      return list;
    }catch (Exception e) {
      //JavaScriptException, ClassCast, and others from gwt.dev.shell
      throw new InvalidFormatException(e);
    }
  }

  /**
   * Returns a list of one or more affordance objs produced by expanding 
   * any compact affordance fields.
   */
  protected static List<Affordance> unpackAffordance(JsAffordance aff) {
    JsArrayMixed affordance = aff.affordance();
    List<String> scratch = new ArrayList<String>(affordance.length());
    List<Affordance> affordances = new LinkedList<Affordance>();
    expand(aff, affordance, 0, scratch, affordances);    
    return affordances;
  }

  /**
   * Recursively works through the affordance's nested/packed display elements,
   * expanding each array-based element.  Loads given affordances list, 
   * using scratch along the way. 
   */
  private static void expand(JsAffordance aff, JsArrayMixed elements, 
                             int elemIndex, 
                             List<String> scratch, 
                             List<Affordance> affordances) {
    
    if (elemIndex == elements.length()) {
      //reached end of affordance
      affordances.add(new PcAction(scratch, aff.cmd(), aff.order(), aff.weak()));
      return;
    }else {
      // NOTE: originally relied only on ClassCastException to check types here.
      // Worked fine in GWT, but not when compiled. Explicit instanceof check
      // works.  (JavaScript test did not work.)
      Object elem;
      try {
        elem = elements.getObject(elemIndex);
      }catch (ClassCastException e){
        elem = elements.getString(elemIndex);
      }
      if (elem instanceof String) {
        scratch.add(elements.getString(elemIndex));
        expand(aff, elements, elemIndex + 1, scratch, affordances);
        scratch.remove(scratch.size() - 1);
      }else {
        //an array of Strings
        JsArrayString strs = elements.getObject(elemIndex);
        for (int e = 0; e < strs.length(); e++) {
          scratch.add(strs.get(e));
          expand(aff, elements, elemIndex + 1, scratch, affordances);
          scratch.remove(scratch.size() - 1);
        }        
      }   
    }    
  }    
}
